#! /usr/bin/env python

from __future__ import with_statement

import sys
import os
import stat
import math
import random
import subprocess
import threading
from tkMessageBox import showwarning
from Tkinter import *
from optparse import OptionParser
from getpass import getuser
from reportlab.lib import colors
from reportlab.lib.units import inch
from reportlab.platypus import SimpleDocTemplate, Table
import config


class OpType:
    """Enum of operation types"""    
    GRADE, STAMP, INFO, EXPORT = range(4)  

class LockHolder:
    """Container for currently held lock."""
    lockf = None
    resource_path = None

class InputError(Exception):
    """Exception raised when the user provides invalid options."""
    
    def __init__(self, msg):
        self.msg = msg
        
    def __str__(self):
        return self.msg

        
class ConfigurationError(Exception):
    """Exception raised when the config file has insufficient or incorrect 
    information."""
    
    def __init__(self, msg):
        self.msg = msg
        
    def __str__(self):
        return "Problem with configuration. " + self.msg

class QuietExecutor(threading.Thread):
    """Thread for silently executing an external program. A resourse to lock
    while the thread is executing may optionally be specified."""
    
    def __init__(self, wd, cmd, to_lock=None):
        threading.Thread.__init__(self)
        self.wd = wd
        self.cmd = cmd
        self.to_lock = to_lock
         
    def run (self):
        if self.to_lock:
            lockf = lock_resource(self.to_lock)
            if not lockf:
                return
        devnull = open('/dev/null', 'w')
        p = subprocess.Popen(self.cmd, shell=True, cwd=self.wd, stderr=devnull)
        p.wait()
        devnull.close()
        if self.to_lock:
            unlock_resource(lockf, self.to_lock)
        
class GraderGui(Tk):
    """A GUI for grading an assingment."""
    def __init__(self, points_possible, current_grades):
        Tk.__init__(self)
        self.points_possible = points_possible
        self.initial_grades = current_grades
        self.grade_entries = {}
        self.initialize()
        
    def initialize(self):
        self.grid()
        parts = self.points_possible.keys()
        parts.sort()
        for i, part in enumerate(parts):
            l = Label(self, text=part)
            l.grid(column=0, row=i, sticky=E)
            e = Entry(self, width=3)
            e.grid(column=1, row=i)
            if part in self.initial_grades:
                e.insert(0, self.initial_grades[part])
            self.grade_entries[part] = e
            l = Label(self, text=" / " + str(self.points_possible[part]))
            l.grid(column=2, row=i, sticky=W)
        b = Button(self, text="Reset", command=self.OnResetButtonClick)
        b.grid(column=0, row=len(parts)+1)
        b = Button(self, text="Submit", command=self.OnSubmitButtonClick)
        b.grid(column=2, row=len(parts)+1)
    
    def OnResetButtonClick(self):
        for part, entry in self.grade_entries.iteritems():
            entry.delete(0, END)
            if part in self.initial_grades:
                entry.insert(0, self.initial_grades[part])
        
    def OnSubmitButtonClick(self):
        try:
            testint = [int(entry.get()) for entry in self.grade_entries.values() if entry.get()]
        except ValueError:
            showwarning("Invalid Input", "Grades must be numbers.")
        else:
            self.quit() 
        
    def prompt_for_grades(self, title="Visual Grader"):
        self.title(title)
        self.mainloop()
        part_to_grade = dict((part, entry.get()) for part, entry in self.grade_entries.iteritems() if entry.get())
        return part_to_grade


class LogManager:
    """A grade log manager that can add grade entries to, and interpret a grade 
    log file. Each entry (line) in the log file will have the format:
        "<assignment>" "<section>" <student> <score> <grader> <user>
    Entries will never be deleted during normal operation so that a full 
    grading history can be recovered."""
    def __init__(self, log_loc):
        self.log_loc = log_loc
        self.lock_loc = log_loc[:log_loc.rfind('/')] + '/.loglock'
        self.lockf = None
        if not os.path.isfile(log_loc):
            newf = self.open_log('w')
            newf.close()

    def log_grade(self, asgn, part, student, score, grader, user):
        """Add a grade entry to the end of the log file."""
        if not self.lock(): return
        asgn = '"' + asgn + '"'
        part = '"' + part + '"'
        with open(self.log_loc, 'a') as logf:
            logf.write(asgn+' '+part+' '+student+' '+score+' '+grader+' '+user+'\n')
        if not self.unlock():
            print "ERROR: Failed to unlock the log. You may need to delete "+self.lock_loc
   
    def get_grades(self):
        """Read the entire log into memory."""
        grade_records = []
        with open(self.log_loc) as logf:
            for line in logf:
                endasgn = line.find('"', 1)
                asgn = line[1:endasgn]
                endpart = line.find('"', endasgn+3)
                part = line[endasgn+3: endpart]
                record = line.split(' ')
                student, score, grader, user = record[-4:]
                grade_records.append((asgn, part, student, int(score), grader, user))
        return grade_records
        
    def find_ungraded_students(self, asgn, part, all_students):
        """Find and return the list of students who have not been graded on 
        the specified part of the specified assignment."""
        graded = set()
        grade_records = self.get_grades()
        for rec_asgn, rec_part, rec_student, a, b, c in grade_records:
            if rec_asgn == asgn and rec_part == part:
                graded.add(rec_student)
        return list(set(all_students) - graded)  
        
    def get_student_grades(self, username, grade_records=None):
        """Generate and return a map from asgn->part->grade for the 
        specified student."""
        asgn_to_part_to_grade = {}
        if not grade_records: grade_records = self.get_grades()
        for rec_asgn, rec_part, rec_student, rec_score, a, b in grade_records:
            if rec_student == username:
                if rec_asgn in asgn_to_part_to_grade:
                    asgn_to_part_to_grade[rec_asgn][rec_part] = rec_score
                else:
                    asgn_to_part_to_grade[rec_asgn] = {rec_part:rec_score}
        return asgn_to_part_to_grade
        
    def get_asgn_grades_by_part(self, assignment, grade_records=None):
        """Generate and return a map from part->student->grade for the 
        specified assignment."""
        part_to_student_to_grade = {}
        if not grade_records: grade_records = self.get_grades()
        for rec_asgn, rec_part, rec_student, rec_score, a, b in grade_records:
            if rec_asgn == assignment:
                if rec_part in part_to_student_to_grade:
                    part_to_student_to_grade[rec_part][rec_student] = rec_score
                else:
                    part_to_student_to_grade[rec_part] = {rec_student:rec_score}
        return part_to_student_to_grade 
        
    def get_asgn_grades_by_student(self, assignment, grade_records=None):
        """Generate and return a map from student->part->(grade, grader) for the 
        specified assignment."""
        student_to_part_to_grade = {}
        if not grade_records: grade_records = self.get_grades()
        for rec_asgn, rec_part, rec_student, rec_score, rec_grader in grade_records:
            if asgn == assignment:
                if student in student_to_part_to_grade:
                    student_to_part_to_grade[rec_student][rec_part] = (rec_score, rec_grader)
                else:
                    student_to_part_to_grade[rec_student] = {rec_part : (rec_score, rec_grader)}
        return student_to_part_to_grade       
    
    #TODO: bad to have these be dependent on random other functions        
    def lock(self):
        self.lockf = lock_resource(self.log_loc)
        if self.lockf: return True
        else: return False
            
    def unlock(self):
        if unlock_resource(self.lockf, self.log_loc): return True
        else: return False


class PdfHelper:
    """Helper for performing various operations on PDFs."""
    
    def __init__(self, handin_dir):
        self.handin_dir = handin_dir
    
    def create_stamp(self, student, grade_map, points_map):
        """Create a grade table stamp for the given assignment and student in
        a file named stamp.pdf in the approrate handin directory."""
        data = None
        cdims = rdims = None
        table = None
        ts = None
        doc = None
        studir = self.handin_dir + '/' + student
        
        if student not in grade_map:
            raise ValueError("No grades available for student " + student)
        scores = grade_map[student]
        data = [['section', 'grade', 'out of', 'grader']]
        ks = scores.keys()
        ks.sort()
        for section in ks:
            grade, grader = scores[section]
            data.append([section, grade, points_map[section], grader])
        ts = [('ALIGN', (0,0), (-1,-1), 'CENTER'),
              ('LINEABOVE', (0,0), (-1,0), 1, colors.black),
              ('LINEBELOW', (0,0), (-1,0), 1, colors.black),
              ('FONT', (0,0), (-1,0), 'Times-Bold')]
        doc = SimpleDocTemplate(studir+'/stamp.pdf', 
                                pagesize=(8.5*inch, (3+len(data)*0.25)*inch))
        cdims = [0.75*inch, 0.5*inch, 0.5*inch, inch]
        rdims = len(data)*[0.25*inch]
        table=Table(data, cdims, rdims, style=ts)
        doc.build([table])
        os.chown(studir + '/stamp.pdf', -1, 3051) # 3051 is cs166ta
        os.chmod(studir + '/stamp.pdf', 
                 stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IWGRP)
        
    def merge_all(self, student, outf):
        wd = self.handin_dir + '/' + student
        handin_files = os.listdir(wd)
        pdfs = ['"' + f + '"' for f in handin_files if f != "stamp.pdf" and f.endswith(".pdf")]
        cmd = "pdftk stamp.pdf " + ' '.join(pdfs) + " cat output " + outf
        print cmd
        p = subprocess.Popen(cmd, shell=True, cwd=wd)  
        p.wait()
        print 'done'
        os.chown(wd+'/'+outf, -1, 3051) # 3051 is cs166ta
        os.chmod(wd+'/'+outf, 
                 stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IWGRP)

                 
class StatUtils:
    """Utils for computing statistical results. Methods are all static."""
    
    @staticmethod
    def compute_average(list):
        return sum(list)/float(len(list))
    
    @staticmethod
    def find_median(list):
        """Naive implementation of median find. Presumably inputs are not 
        large enough to justify the complexity of the O(n) implementation."""
        list.sort()
        return list[len(list)/2]
    
    @staticmethod  
    def compute_stdev(list, average=None):
        """Return the standard deviation of the given list."""
        if not average: average = StatUtils.compute_average(list)
        sum_squared_dif = sum(math.pow(x - average, 2) for x in list)
        return math.sqrt(sum_squared_dif / float(len(list)))
        
        
################################################################################
# Helper functions
################################################################################

def lock_resource(path):
    lock_loc = path + '.lock'
    while is_resource_locked(path):
        resp = raw_input(path + ' is locked for another user. Try again? (y/n)')
        if resp == 'n':
            print 'k thanks bye'
            return None
        elif not resp == 'y':
            print "Try 'y' or 'n' next time." 
    try:
        lockf = open(lock_loc, 'w')
        LockHolder.lockf, LockHolder.resource_path = lockf, path
        return lockf
    except IOError: 
        print "ERROR: Could not create lock at: " + lock_loc
        exit(1)
        
def unlock_resource(lockf, path):
    lock_loc = path + '.lock'
    if lockf:
        lockf.close()
        os.remove(lock_loc)
        LockHolder.lockf = LockHolder.resource_path = None
        return True
    return False     

def is_resource_locked(path):
    return os.path.isfile(path + '.lock')
    
def prompt_for_section(allowed, msg=None, optional=False):
    if not msg:
        msg = "Enter an assignment section: "
    section = raw_input(msg)
    while section not in allowed:
        if not section and optional: break
        print "Valid sections are: "
        print ",".join(allowed)
        section = raw_input(msg)
    return section

################################################################################
# Main script functions
################################################################################

def grade_asgn(options):
    """Add entries to the grades log for the specified options.
    
    Input:
    - options.asgn: The assignment to grade
    - options.username: The username of the student to grade
    - options.points_possible: A mapping from part->points possible for the 
        specified assignment.
    - options.grader: The username of the TA responsible for entering this grade
    - options.open: True to open handin files. Requires:
        - options.viewers: A map from file extesion to executable to 
            be used to open handin files of the given type. 
        - options.handin_dir: The path to the handin directory for the
            specified assignment.
    - options.comment: True to open comments file. Requires:        
        - options.editor: Specifies the executable used to edit comments.
        - options.comments_dir: Specifies the directory where the
            graders comments on the assignment/student will be stored.
    - options.part: OPTIONAL. The part/problem of the assignment to grade
    - options.score: OPTIONAL. The score to assign. options.part required.
    - options.visual: OPTIONAL. Indicates that the gui grader should be used."""
  
    if options.score:
        options.lm.log_grade(options.asgn, options.part, options.username, 
                options.score, options.grader, getuser())
        return
    
    p = None
    if options.open:
        wd = options.handin_dir + '/' + options.username
        handins = os.listdir(wd)
        for ext, viewer in options.viewers.items():
            matching_handins = [f for f in handins if f.endswith("." + ext)]
            if matching_handins:
                cmd = viewer + ' ' + ' '.join(matching_handins)
                lock_path = wd + '/' + ext
                QuietExecutor(wd, cmd, lock_path).start()
    
    if options.comment:
        wd = options.comments_dir
        cmd = options.editor + " " + options.username + "_comments.txt"
        lock_path = wd + "/" + options.username + "_comments.txt"
        QuietExecutor(wd, cmd, lock_path).start()
    
    if options.visual:
        current_grades = {}
        all_current_grades = options.lm.get_student_grades(options.username)
        if options.asgn in all_current_grades:
            current_grades = all_current_grades[options.asgn]
        vg = VisualGrader(options.points_possible, current_grades)
        part_to_grade = vg.prompt_for_grades(options.username)
        for part, grade in part_to_grade.iteritems():
            if part not in current_grades or current_grades[part] != grade:
                options.lm.log_grade(options.asgn, part, options.username,
                        str(grade), options.grader, getuser())
    else:    
        if not options.part:
            options.part = prompt_for_section(options.points_possible.keys())
        while options.part:
            score = raw_input("Grade out of " + str(options.points_possible[options.part]) + 
                    " for " + options.username + " for " + options.asgn + 
                    ", section " + options.part + ": ")
            try:
                testint = int(score)
            except ValueError:
                print "ERROR: Please enter a number as the section grade."
            else:
                options.lm.log_grade(options.asgn, options.part, 
                        options.username, score, options.grader, getuser())
                options.part = prompt_for_section(options.points_possible.keys(), 
                            "Also grade section (or ENTER to finish): ", True)
 
            
def stamp_handins(options):
    """Append pdf grade tables to the beginning of each students' handin for
    the specified assignment to for handback pdfs.
    
    Input:
    - options.asgn: The assginment to create handback pdfs for.
    - options.handin_dir: The path to the handin directory for the assignment.
    - options.students_list: A list of all students who handed in the assignment.
    - options.points_possible: A map from part->points possible.
    - options.lm: The log manager.
    """
    pdfhelper = PdfHelper(options.handin_dir)
    grade_map = options.lm.get_asgn_grades_by_student(options.asgn)
    for student in options.students_list:
        pdfhelper.create_stamp(student, grade_map, options.points_possible)
        pdfhelper.merge_all(student, "handback.pdf")


def print_info(options):
    """Lookup and print grading information for an assignment or student.
    
    Input:
    - options.points_possible: A map from asgn->part->points possible with 
        entries for each assignment and part that info has be requested for.
    - options.lm: The log manager.
    - Either:
        - options.username: The username of the student to get info for.
      Or
        - options.students_list: The list of all students whose grades to 
            aggregate.
    """
    if options.username:
        print_student_info(options.username, options.points_possible, options.lm)
    else:
        print_assignment_info(options.points_possible, options.students_list, options.lm)
 
                
def print_student_info(student, points_possible, lm):
    """Lookup and print grading info for the specified student on all of the 
    assignments and parts specified."""
    print "Grades for " + student + ":"
    asgn_to_part_to_grade = lm.get_student_grades(student)
    assignments = points_possible.keys()
    assignments.sort()
    for asgn in assignments:
        print "##  Assignment " + asgn + ":"
        if asgn not in asgn_to_part_to_grade:
            print "    No grades available."
            continue
        parts = points_possible[asgn].keys()
        parts.sort()
        for part in parts:
            if part not in asgn_to_part_to_grade[asgn]:
                print "    Part " + part + ": No grade available."
                continue
            grade = asgn_to_part_to_grade[asgn][part]
            possible = points_possible[asgn][part]
            print "    Part %s: %d / %d" % (part, grade, possible)
        total = sum(asgn_to_part_to_grade[asgn].values()) 
        possible = sum(points_possible[asgn].values())  
        print "    Total: %d / %d " % (total, possible)
  
            
def print_assignment_info(points_possible, all_students, lm):
    """Lookup and print grading info for the specified assignment."""
    student_to_totalgrade = dict((student, 0) for student in all_students)
    assignments = points_possible.keys()
    assignments.sort()
    for asgn in assignments:
        print "Grades for " + asgn + ":"
        missing = []
        part_to_student_to_grade = lm.get_asgn_grades_by_part(asgn)
        parts = points_possible[asgn].keys()
        parts.sort()
        for part in parts:
            print "##  Part " + part + ":"
            if part not in part_to_student_to_grade:
                print "    No grades available."
                missing += [(part, student) for student in all_students]
                continue
            graded_students = part_to_student_to_grade[part].keys()
            missing += [(part, student) for student in (set(all_students) - set(graded_students))]
            for student, grade in part_to_student_to_grade[part].iteritems():
                if student not in student_to_totalgrade:
                    print "WARNING: student " + student + " has a grade but no handin, or is not listed as a student!"
                    continue
                student_to_totalgrade[student] += int(grade)
            grades = part_to_student_to_grade[part].values()
            ave = StatUtils.compute_average(grades)
            med = StatUtils.find_median(grades)
            stdev = StatUtils.compute_stdev(grades)
            print "    average: %.1f / %d" % (ave, points_possible[asgn][part])
            print "    median: %d / %d" % (med, points_possible[asgn][part])
            print "    standard deviation: %.2f" % stdev
        grades = student_to_totalgrade.values()
        ave = StatUtils.compute_average(grades)
        med = StatUtils.find_median(grades)
        stdev = StatUtils.compute_stdev(grades)
        print "##  Total:"
        print "    average: %.1f / %d" % (ave, sum(points_possible[asgn].values()))
        print "    median: %d / %d" % (med, sum(points_possible[asgn].values()))
        print "    standard deviation: %.2f" % stdev
       
        if missing:
            print ""
            print "Missing grades for assignment " + asgn + " (<part> <student>):"
            for part, student in missing:
                print part, student


def export_to_csv(options):
    """Write out the grades for an assignment to a csv file.
    
    Input:
    - options.export: The path where the csv file should be written.
    - options.asgn: The assignment to export the grades for.
    - options.students_list: The list of all students who handed in.
    - options.points_possible: A map from part->points possible for the 
        assignment.
    - options.lm: The log manager.
    """
    student_to_grades = dict((student, []) for student in options.students_list)
    with open(options.export, 'w') as exportf:
        parts = options.points_possible.keys()
        parts.sort()
        part_to_student_to_grade = options.lm.get_asgn_grades_by_part(options.asgn)
        exportf.write('section ' + ' '.join(parts) + '\n')
        outof = 'possible'
        for part in parts:
            outof += ' ' + str(options.points_possible[part])
            if part not in part_to_student_to_grade:
                for student in options.students_list:
                    student_to_grades[student].append('NaN')
            else:
                student_to_grade = part_to_student_to_grade[part]
                for student in options.students_list:
                    if student in student_to_grade:
                        grade = student_to_grade[student]
                        student_to_grades[student].append(grade)
                    else:
                        student_to_grades[student].append('NaN')
        
        exportf.write(outof + '\n')
        students = student_to_grades.keys()
        students.sort()
        for student in students:
            exportf.write(student + ' ' + ' '.join(student_to_grades[student]) + '\n')

################################################################################
# Validation and Setup Operations
################################################################################

def validate_and_get_optype(options):
    """Validates the user's input, and returns the type of operation that
    should be performed in response. 
    
    Raises: - InputError if the specified options are invalid."""
    msg = ""
    # If the 'info' flag is set, one or both of 'asgn' and 'username' must be
    # specified. If 'asgn' is set, 'part' may optionally be set as well.
    if options.info:
        if not (options.asgn or options.username):
            raise InputError("Invalid Input. -u (student username) or -a (assignment) options required with -i (info) option.")
        if options.part and not options.asgn:
            raise InputError("Invalid Input. -a (assignment) option required with -p (part) option.")
        optype = OpType.INFO
        
    
    # If the 'stamp' flag is set, then 'asgn' must be specified
    elif options.stamp:
        if not options.asgn:
            raise InputError("Invalid Input. -a (assignment) option required with -s (stamp) option.")
        optype = OpType.STAMP
    
    # If 'export' is set, then 'asgn' must be specified
    elif options.export:
        if not options.asgn:
            raise InputError("Invalid Input. -a (assignment) option required with -e (export) option.")
        optype = OpType.EXPORT
        
    # If an 'asgn' was specified, then 'username' or the 'next' flag must
    # be specified, with optional 'part', 'score', 'visual', and 'comment'
    # options.
    elif options.asgn:
        if options.username and options.next:
            raise InputError("Invalid Input. -u (student username) and -n (next) options are mutually exclusive.")
        if not (options.username or options.next):
            raise InputError("Invalid Input. -u (student username) or -n (next) options required to grade an assignment")
        if options.next and not options.part:
            raise InputError("Invalid Input. -p (part) option required with -n (next) option.")
    
        optype = OpType.GRADE
    
    # Not enough input options to make a valid command
    else:
        raise InputError("Invalid Input. Not enough options supplied. Run 'pygrade -h' for usage.")
            
    return optype

def lookup_missing_params(optype, options):
    """Fill in missing options for the given optype based on the defaults and
    other information provided in the configuration file, and check that the 
    user's input is compatible with this configuration. 
    
    Raises: - ConfigurationError if any required defaults are missing from the 
              configuration file."""
    msg = None
    log_path = options.logfile
    if not log_path:
        log_path = config.logfile
    if not os.path.isfile(log_path):
        resp = raw_input("Log file " + log_path + " does not exist. Create it? (y/n)")
        if resp == 'y':
            f_log = open(log_path, 'a')
            f_log.close()
        else:
            print "Ok. Exiting."

    # Note: Although many of the required parameters for these ops are common, 
    # the cases are kept seperate here to allow for easy future adaptations
    
    if optype == OpType.GRADE:
        students_list = get_students_list(options.asgn)
        options.lm = LogManager(log_path)
        options.points_possible = get_points_possible(options.asgn)
        options.grader = getuser()
        if options.part and options.part not in options.points_possible:
            raise ConfigurationError("Part " + options.part + " for assignment " + options.asgn + " not present")
        if options.open:
            options.handin_dir = get_handin_dir(options.asgn)
            options.viewers = config.viewers
        if options.comment:
            options.comments_dir = get_comments_dir(options.asgn)
            options.editor = config.editor
        if options.username:
            if options.username not in students_list:
                print "student " + options.username + " has no handin for assignment " + options.asgn + " or is not listed as a student in config file."
                exit(0)
        if options.next:
            ungraded_students = options.lm.find_ungraded_students(options.asgn, 
                    options.part, students_list)
            if ungraded_students:
                options.username = ungraded_students[int(random.random()*len(ungraded_students))]
            else:
                print "No remaining students to grade"
                exit(0)
                            
    elif optype == OpType.INFO:
        options.lm = LogManager(log_path)
        if not options.username:
            options.students_list = get_students_list(options.asgn)
        if options.asgn:
            try:
                options.points_possible = {options.asgn:get_points_possible(options.asgn)}
            except KeyError:
                raise ConfigurationError("Assignment " + options.asgn + " not present")
        else:
            options.points_possible = config.points_possible
        
    elif optype == OpType.STAMP:
        options.lm = LogManager(log_path)
        options.students_list = get_students_list(options.asgn)
        options.handin_dir = get_handin_dir(options.asgn)
        options.points_possible = get_points_possible(options.asgn)
        
    elif optype == OpType.EXPORT:
        options.lm = LogManager(log_path)
        options.points_possible = get_points_possible(options.asgn)
        options.students_list = get_students_list(options.asgn)

def get_points_possible(asgn):
    try:
        return config.points_possible[options.asgn]
    except KeyError:
        raise ConfigurationError("Assignment " + options.asgn + " not present")

def get_handin_dir(asgn):
    """Return the path to the hadnin directory for the specified assignment. 
    
    Raises: - ConfigurationError if handin directory is incorrect or missing.
            - InputError if no subdirectory exists for the specified 
              assignment.""" 
    handin_dir = config.handin_dir
    if not os.path.isdir(handin_dir):
        raise ConfigurationError("Handin location " + handin_dir + " is not a directory.")
    handin_dir +=  asgn
    if not os.path.isdir(handin_dir):
        raise InputError("Assignment " + asgn + " does not have a handin directory at " + handin_dir + ".")
    return handin_dir

def get_comments_dir(asgn):
    """Return the path to the comments directory for the specified assignment. 
    
    Raises: - ConfigurationError if comments directory does not exist.""" 
    comments_dir = config.comments_dir
    if not os.path.isdir(comments_dir):
        raise ConfigurationError("Comments location " + comments_dir + " is not a directory.")
    comments_dir +=  asgn
    if not os.path.isdir(comments_dir):
        os.mkdir(comments_dir)
    return comments_dir

def get_students_list(asgn):
    """Return the list of students who have handed in the given assignment and 
    who are registered to take the course.
    
    Raises: ConfigurationError if handin directory or students list
            is incorrect or missing."""
    handins_list = None
    handin_dir = get_handin_dir(asgn)
    handins_list = os.listdir(handin_dir)
    if not handins_list:
        print "WARNING. No handins found in directory " + handin_dir
    return list(set(config.enrolled) & set(handins_list))

def describe_operation(optype, options):
    """Return a description of the specified operation, for display to the 
    user."""
    msg = ""
    if optype == OpType.INFO:
        msg = "Action: retrieve info for "
        if options.username:
            msg += "student " + options.username + ", "
        if options.asgn:
            msg += "assignment " + options.asgn + ", "
        if options.part:
            msg += "part " + options.part
        msg = msg.strip(", ") + "."
    elif optype == OpType.STAMP:
        msg = "Action: stamp handins for assignment " + options.asgn + "."
    elif optype == OpType.GRADE:
        msg = "Action: Assign grade "
        if options.score:
            msg += options.score + " "
        if options.part:
            msg += "out of " + str(options.points_possible[options.part]) + " "
        msg += "for assignment " + options.asgn + ", "
        if options.part:
            msg += "part " + options.part + ", "
        if options.username:
            msg += "for student " + options.username + " "
    return msg
    
if __name__ == "__main__": 
    parser = OptionParser()
    parser.add_option("-a", "--asgn", action="store",
                      help="the assignment name")
    parser.add_option("-p", "--part", action="store",
                      help="the part of the assignment to grade")
    parser.add_option("-u", "--username", action="store",
                      help="the username of the student to grade")
    parser.add_option("-o", "--open", action="store_true", default=False,
                      help="open handin files for viewing/editing [default:%default]")
    parser.add_option("-c", "--comment", action="store_true", default=False,
                      help="open an editor to make comments. [default: %default]")
    parser.add_option("-n", "--next", action="store_true", default=False,
                      help="grade the next student without a score. [default: %default]")
    parser.add_option("-g", "--grader", action="store",
                      help="the username of the grader [default: current user]")
    parser.add_option("-f", "--logfile", action="store",
                      help="the file to use as the grade log [default: specified in .grade.conf]")  
    parser.add_option("-s", "--stamp", action="store_true", default=False,
                      help="stamp the handin/s with a generated pdf grade table") 
    parser.add_option("-e", "--export", action="store",
                      help="export the grade log in to the specified location in CSV format")
    parser.add_option("-i", "--info", action="store_true", default=False,
                      help="get information about grades")
    parser.add_option("-v", "--visual", action="store_true", default=False,
                      help="display a grading gui for the assignment")
    
    options = parser.parse_args()[0]
    options.score = None
    options.lm = None
    try:
        optype = validate_and_get_optype(options)
        try:
            lookup_missing_params(optype, options)
        except NameError, e:
            a, name, b = str(e).split("'")
            raise ConfigurationError("Var '" + name + "' not specified.")
    except (InputError, ConfigurationError), e:
        print e
        exit(1)

    # print a description of the operation about to be performed
    print describe_operation(optype, options)
    
    try:
        if optype == OpType.GRADE:
            grade_asgn(options)
        elif optype == OpType.INFO:
            print_info(options)
        elif optype == OpType.STAMP:
            stamp_handins(options)
        elif optype == OpType.EXPORT:
            export_to_csv(options)
    except KeyboardInterrupt:
        if LockHolder.lockf:
            if not unlock_resource(LockHolder.lockf, LockHolder.resource_path):
                print "Warning. Unable to unlock resource at " + LockHolder.resource_path
        
